/*
* Copyright (c) 2014 Qunar.com. All Rights Reserved.
*/
package net.oschina.bilbodai.common.beanutil.core;

import net.oschina.bilbodai.common.beanutil.runtime.IRuntimeClassLoader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.CheckClassAdapter;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicLong;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Type.getInternalName;

/**
 * @author maodai  Date: 15-8-4 Time: 下午2:34
 * @version $Id$
 */
public abstract class ASMCodeGenerator {

    private static final String BEAN_UTIL_RUNTIME_BEAN_PREFIX = "BU";
    private final int version;
    private final String className;
    private final ClassWriter classWriter;
    private static final AtomicLong CLASS_INDEX_COUNTER = new AtomicLong();
    private final String superClazzInternal;
    private final int access;
    private String[] interfacesInternal;
    private ClassVisitor cw;

    public ASMCodeGenerator(String customClsName, int version, Class<?> superClazz, Class<?>... interfaces) {
        this(customClsName, version, null, superClazz, interfaces);
    }

    public ASMCodeGenerator(String customClsName, int version, PrintWriter writer, Class<?> superClazz, Class<?>... interfaces) {
        this(version, BEAN_UTIL_RUNTIME_BEAN_PREFIX, customClsName, writer, superClazz, interfaces);
    }

    public ASMCodeGenerator(int version, String namePrefix, String customClsName, PrintWriter writer, Class<?> superClazz, Class<?>... interfaces) {
        this.version = version;
        this.className = namePrefix + "$" + customClsName + "$" + System.currentTimeMillis() + "_" + CLASS_INDEX_COUNTER.incrementAndGet();
        classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        CheckClassAdapter checkClassAdapter = new CheckClassAdapter(classWriter, false);
        if (writer != null) {
            cw = new TraceClassVisitor(checkClassAdapter, writer);
        } else {
            cw = checkClassAdapter;
        }

        this.superClazzInternal = superClazz == null ? getInternalName(Object.class) : getInternalName(superClazz);
        this.access = superClazz == null ? ACC_PUBLIC : ACC_PUBLIC + ACC_SUPER;

        if (interfaces != null) {
            this.interfacesInternal = new String[interfaces.length];
            for (int i = 0; i < interfaces.length; i++) {
                this.interfacesInternal[i] = Type.getInternalName(interfaces[i]);
            }
        }
    }

    private void generateClassName(ClassVisitor cw) {
        cw.visit(version, access, this.className, null, superClazzInternal, this.interfacesInternal);
    }

    private void generateConstructor(ClassVisitor cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, superClazzInternal, "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
    }

    public abstract void onVisitorMethods(ClassVisitor cw, Object... args) throws Exception;

    public Class<?> getLoadedClass(IRuntimeClassLoader classLoader, Object... args) throws Exception {
        return classLoader.defineClass(this.className, getUnloadedClass(args));
    }

    public byte[] getUnloadedClass(Object... args) throws Exception {
        generateClassName(this.cw);
        generateConstructor(this.cw);
        onVisitorMethods(this.cw, args);
        return visitClassEnd();
    }

    private byte[] visitClassEnd() {
        this.cw.visitEnd();
        return this.classWriter.toByteArray();
    }

    public String getClassName() {
        return className;
    }
}